package RSA;
import java.io.*;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.BadPaddingException;

public class RSA extends Thread{

	public static int KEY_SIZE = 1024;
	public static String ALGORITHM = "RSA";
	
	//public RSAPublicKeySpec publicKey_Spec;
	//public RSAPrivateKeySpec privateKey_Spec;
	/*
	private PublicKey publicKey;
	private PrivateKey privateKey;
	*/
	private Key publicKey;
	private Key privateKey;
	
	private static int keySize;
	
	private boolean m_randomSize = false;
	/**
	 * Constructeur sans argument
	 * 
	 * Quand celui-ci est appelé des clés publiques et privées sont créées
	 */
	public RSA()
	{
		keySize=256;
		generateKeys();
	}
	
	public RSA(int kl)
	{
		keySize=kl;
		generateKeys(kl);
	}
	
	/**
	 * Constructeur spécifiant le fichier qui contient la clé public
	 * @param pubKeyFilename le nom du fichier qui contient la clé public
	 */
	public RSA(String pubKeyFilename)
	{
		try {
			readKeyFromFile(pubKeyFilename);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Fonction qui génère une paire de clés (Public et privée)
	 * 
	 */
	public void generateKeys(){

		KeyPairGenerator kpg = new KeyPairGenerator(keySize);
		publicKey = kpg.getPublicKey();
		privateKey = kpg.getPrivateKey();
		
		
		//TestKeys();
		//System.out.println("Une paire de clés a été créée");
	}
	
	/**
	 * Fonction qui génère une paire de clés (Public et privée) d'une certaine length
	 * 
	 */
	public void generateKeys(int length){

		keySize =  length;
		KeyPairGenerator kpg = new KeyPairGenerator(keySize);
		publicKey = kpg.getPublicKey();
		privateKey = kpg.getPrivateKey();
		//System.out.println("Une paire de clés a été créée");
	}
	
	/**
	 * 
	 * @return la clé publique
	 */
	public Key getPublicKey()
	{
		return publicKey;
	}
	
	/**
	 * set la publicKey
	 */
	public void setPublicKey(Key pub)
	{
		keySize = pub.getSize();
		publicKey = pub;
	}
	
	/**
	 * Lit une clé publique a partir d'un fichier
	 * @param filename le nom du fichier où se situe la clé publique
	 * @throws IOException
	 */
	public void readKeyFromFile(String filename) throws IOException 
	{		  
		  ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filename)));
		  try 
		  {
		    BigInteger m = (BigInteger) oin.readObject();
		    BigInteger e = (BigInteger) oin.readObject();
		    publicKey = new Key(m,e,keySize);
		  } 
		  catch (Exception e) 
		  {
		    throw new RuntimeException("Spurious serialisation error", e);
		  } 
		  finally 
		  {
		    oin.close();
		  }
	}
	
	/**
	 * Cette fonction récupère une clé privée à partir d'un fichier
	 * @param filename le nom du fichier ou se situe la clé privée
	 * @throws IOException
	 */
	public void readPrivateKeyFromFile(String filename) throws IOException 
	{		  
		  ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(filename)));
		  try 
		  {
		    BigInteger m = (BigInteger) oin.readObject();
		    BigInteger e = (BigInteger) oin.readObject();
		    privateKey = new Key(m,e,keySize);
		  } 
		  catch (Exception e) 
		  {
		    throw new RuntimeException("Spurious serialisation error", e);
		  } 
		  finally 
		  {
		    oin.close();
		  }
	}
	
	/**
	 * Permet de sauvegarder la clé publique sous le nom 'filename'
	 * @param filename
	 * @throws IOException
	 */
	public void savePublicKeyToFile(String filename) throws IOException
	{
		saveToFile(filename, publicKey.getModulus(), publicKey.getExponent());
	}
	
	/**
	 * Permet de sauvegarder la clé privée sous le nom 'filename'
	 * @param filename
	 * @throws IOException
	 */
	public void savePrivateKeyToFile(String filename) throws IOException
	{
		saveToFile(filename, privateKey.getModulus(), privateKey.getExponent());
	}
	
	/**
	 * Méthode générique qui permet de sauvegarder une clé RSA (modulo, exposant) dans un fichier
	 * @param filename
	 * @param mod
	 * @param exp
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public static void saveToFile(String filename, BigInteger mod, BigInteger exp) throws FileNotFoundException, IOException
	{
		ObjectOutputStream oout = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(filename)));
		  try {
		    oout.writeObject(mod);
		    oout.writeObject(exp);
		  } catch (Exception e) {
		    throw new IOException("Unexpected error", e);
		  } finally {
		    oout.close();
		  }
	}
	
	/**
	 * Méthode pour crypter un array de byte avec publicKey
	 * @param data, les données à crypter
	 * @return un array de byte avec les données cryptées
	 */
	public byte[] Encrypt(byte[] data)
	{
		//System.out.println("e:"+data.length);
		//Si on veut encoder des données de taille importante, il faut appliquer plusieurs fois le cryptage
		if(data.length >= 8)
		{
			return EncryptBiggerData(data);
		}
		else //Si les données sont de taille raisonable, on chiffre simplement
		{
			byte[] cipherData = null;
			try {
				cipherData = Cipher.Encrypt(data,publicKey);
			} catch (Exception e) {
				e.printStackTrace();
			}
			/*
			System.out.println("=============================="+cipherData.length);
			MonSocket.affiche(cipherData);
			BigInteger[] b = getBigIntegerArrayFromBytes(cipherData);
			for(int i=0; i<b.length;i++) System.out.print(b[i]);
			System.out.println();
			System.out.println("==============================");
			*/
			//System.out.println("e:"+cipherData.length);
			return cipherData;
		}
	}
	
	private byte[] EncryptBiggerData(byte[] data) {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        
        int size = 8;
        byte[] buffer = new byte[size]; ;
        
		try {
			/*
	         * Les instructions suivantes permettent d'éviter les répétitions (on lit un nombre aléatoire (<=8) de byte pour encrypter)
	         */
			
	        if(m_randomSize)
	        {
	        	Random r = new Random();
	        	size = Math.abs(r.nextInt()%8 + 1);
	        	buffer = new byte[size]; 
	        	while (bais.read(buffer) != -1) {
	  	        	//System.out.println(getStringFromBytes(buffer)+" ");
	  	            byte[] buffer2 = Cipher.Encrypt(buffer,publicKey);
	  	            //System.out.print(RSA.getBigIntegerFromBytes(buffer2)+" ");
	  	            //System.out.print("#"+buffer2.length+"#");
	  	            baos.write(buffer2);
	  	            //for(int i=0;i<8;i++) System.out.println(buffer[i]);
	  	            size = Math.abs(r.nextInt()%8 + 1);
	  	            buffer = new byte[size];
	  	        }
	        }
	        else
	        {
	        	while (bais.read(buffer) != -1) 
	        	{
		            byte[] buffer2 = Cipher.Encrypt(buffer,publicKey);
		            baos.write(buffer2);
		            //for(int i=0;i<8;i++) System.out.println(buffer[i]);
		            buffer = new byte[size];
		        }
	        }
	       // System.out.println();
		} catch (Exception e) {
			e.printStackTrace();
		} 
        
        
        buffer = baos.toByteArray();
        //System.out.println(buffer.length);
        //buffer = Arrays.copyOfRange(buffer, 0, data.length);
        return buffer;
	}

	/**
	 * Méthode pour crypter une chaine de caractère
	 * @param chaine à crypter
	 * @return un array de byte avec la chaine cryptées
	 */
	public byte[] EncryptString(String chaine)
	{
		byte[] data = chaine.getBytes();
		return Encrypt(data);
	}
	
	/**
	 * Méthode pour décrypter un array de byte,
	 * ATTENTION cette fonction tentera de décrypter avec la clé privée de l'objet,
	 * 			 Préférez la fonction Decrypt(byte[] data, String filename) si vous avez un fichier contenant la clé privée	
	 * @param data
	 * @return
	 */
	public byte[] Decrypt(byte[] data)
	{
		//System.out.println("d:"+data.length);
		if(data.length >= 8)
		{
			return DecryptBiggerData(data);
		}
		else
		{
			byte[] decipherData = null;
			try {
				if(privateKey == null) throw new Exception("Il n'y a pas de clé privée pour déchiffrer");
				/*
				System.out.println("=============================="+data.length);
				MonSocket.affiche(data);
				System.out.println("==============================");
				*/
				decipherData = Cipher.Decrypt(data,privateKey);
				/*
				System.out.println("=============================="+decipherData.length);
				MonSocket.affiche(decipherData);
				System.out.println("==============================");
				*/
				
			}
			catch (BadPaddingException e1) {
				System.err.println("Vous essayez probablement de décrypter des données avec une mauvaise clé privée.\nLa clé privée ne correspond pas à la clé publique qui a été utilisée pour crypter les données. ");
			} catch (Exception e) {
				e.printStackTrace();
			}
			return decipherData;
		}
	}
	
	private byte[] DecryptBiggerData(byte[] data) {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ByteArrayInputStream bais = new ByteArrayInputStream(data);
		
		//System.out.println(bais.available());
		
		byte[] buffer = new byte[keySize/4];
		try {
		    while (bais.read(buffer) != -1) {
		    	//System.out.println(bais.available());
			    //System.out.print(RSA.getBigIntegerFromBytes(buffer)+" ");
			    
			    byte[] buffer2 = Cipher.Decrypt(buffer,privateKey);
			    //System.out.println(RSA.getBigIntegerFromBytes(buffer2));
			    //System.out.println(getStringFromBytes(buffer2));
			    baos.write(buffer2);
			    for(int i=0;i<keySize/4;i++) buffer[i] =0;
		    }
		    //System.out.println();
		} catch (Exception e) {
			e.printStackTrace();
		} 
		
		
		buffer = baos.toByteArray();
		return buffer;
	}

	/**
	 * Méthode pour décrypter des données à partir d'un fichier de clé privée
	 * @param data
	 * @param filename
	 * @return
	 */
	public byte[] Decrypt(byte[] data, String filename)
	{
		byte[] decipherData = null;
			try {
				readPrivateKeyFromFile(filename);
				
				decipherData = Cipher.Decrypt(data,privateKey);
			}catch (IOException e1) {
				System.err.println("Le fichier '"+filename+"' est introuvable");
			}
			catch (Exception e) {
				System.err.println(e.getMessage());
				e.printStackTrace();
			}
		
		return decipherData;
		
	}
	
	/**
	 * Méthode pour récupérer la chaine de caractère à partir d'un array de byte
	 * @param data
	 * @return
	 */
	public static String getStringFromBytes(byte[] data)
	{
		String chaine = null;
		if(data!=null) chaine = new String(data);
		chaine = chaine.trim();
		String tmp = "";
		for(int i=0;i<chaine.length();i++)
		{
			if(chaine.charAt(i) != 0)
			{
				tmp += chaine.charAt(i);
			}
		}
		chaine = tmp;
		return chaine;
	}
	
	public static String getStringFromBigIntegerArray(BigInteger[] b)
	{
		String chaine = "";
		if(b!=null)
		{
			for(int i=0;i<b.length;i++)
			{
				chaine += (char)b[i].intValue();
			}
		}
		//System.out.println(" <après cryptage> "+chaine);
		return chaine;
	}
	
	public static BigInteger[] getBigIntegerArrayFromString(String s)
	{
		BigInteger[] r = new BigInteger[s.length()];
		//byte[] b = s.getBytes();
		//System.out.println("################################");
		//Test.afficheBytes(b);
		//System.out.println("################################");
		for(int i=0;i<s.length();i++)
		{
			r[i] = BigInteger.valueOf(s.charAt(i));
			//System.out.println("size:"+r[i].bitLength());
			//System.out.print(r[i]+" ");
			
			//r[i] = new BigInteger(Arrays.copyOfRange(b, i, i+3));
		}
		//System.out.println(" <avant cryptage> "+s);
		//Ici la chaine correspond bien !²
		
		
		return r;
	}
	
	public static BigInteger[] getBigIntegerArrayFromBytes(byte[] b)
	{
		String s = getStringFromBytes(b);
		//System.out.println("~~~"+s+"~~~");
		BigInteger[] bi = getBigIntegerArrayFromString(s);

		//System.out.println(b.length);
		BigInteger[] bia = new BigInteger[b.length/4];
		for(int i=0;i<bia.length;i++)
		{	
			bia[i] = new BigInteger(Arrays.copyOfRange(b, i, i+3));
			//System.out.println(bia[i]);
		}
		
		return bi;
		
	}
	
	public static byte[] getBytesFromString(String s)
	{
		byte[] b = new byte[s.length()+(s.length()%8)];
		
		int i=0;
		for(i=0;i<s.length();i++)
		{
			b[i] = (byte)s.charAt(i);
		}
		if(i<b.length)
		{
			String space = " ";
			for(int j=i;j<b.length;j++)
			{
				b[j] = (byte)space.charAt(0);
			}
		}
		
		
		return s.getBytes();
	}
	//Fonctionne !
	public static byte[] getBytesFromBigIntegerArray(BigInteger[] b)
	{
		String s = getStringFromBigIntegerArray(b);
		
		//byte[] bty = new byte[b.length*4];
		
		//return bty;*/
		return s.getBytes();
	}
	
	public static BigInteger getBigIntegerFromBytes(byte[] b)
	{
		return new BigInteger(1, b);
	}

	public static byte[] getBytesFromBigInteger(BigInteger b)
	{
		return regularizeSize(b.toByteArray(),keySize/4);
		//return b.toByteArray();
	}
	
	private static byte[] regularizeSize(byte[] b, int size)
	{
		byte[] tmp = new byte[size];
		int sizeOfb = b.length;
		int offset = size-sizeOfb;
		int i=0;
		while(i<offset)
		{
			tmp[i] = 0;
			i++;
		}
		while(i<size)
		{
			tmp[i] = b[i-(offset)];
			i++;
		}
		
		return tmp;
	}
	
	public int getKeyLength()
	{
		return keySize;
	}
	
	public void setKeyLength(int kl)
	{
		keySize=kl;
	}
	
	public void run()
	{
		generateKeys();
	}
}

